﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DDD {


    /// <summary>
    /// カメラ・コンポーネント
    /// </summary>
    /// <remarks>
    /// ノードにアタッチ可能なコンポーネントの1つでカメラ機能を実装します。
    /// カメラはシーン中に複数存在可能で、そのうちの1つを指定してレンダリングを行います。
    /// </remarks>
    public class Camera : Component {

        #region Field
        ProjectionType type;
        float fovy;
        float aspectRatio;
        float nearClip;
        float farClip;
        Matrix4x4 projMatrix;
        #endregion

        #region Constructor

        /// <summary>
        /// コンストラクター
        /// </summary>
        /// <remarks>
        /// 初期値は未定.
        /// </remarks>
        public Camera () {
            this.type = ProjectionType.Parallel;
            this.fovy = 1;
            this.aspectRatio = 1;
            this.nearClip = 0.1f;
            this.farClip = 100;
            this.projMatrix = Matrix4x4.Identity;
        }
        #endregion

        #region Property

        /// <summary>
        /// カメラタイプ
        /// </summary>
        /// <remarks>
        /// 透視投影(Perspective), 平行投影(Parallel), スクリーン(Screen), 汎用(Generic)のいずれかが返ります。
        /// </remarks>
        public ProjectionType Type {
            get { return type; }
        }

        /// <summary>
        /// カメラのY方向の視野角(Field of View)
        /// </summary>
        /// <remarks>
        /// カメラタイプが透視投影(Perspective)の場合はY方向の視野角(in degree)を返します。
        /// 平行投影の時は視野のカメラ空間での長さ(Y方向)を返します。
        /// それ以外の時は未定義です。
        /// </remarks>
        public float Fovy {
            get { return fovy; }
            private set { this.fovy = value; }
        }

        /// <summary>
        /// アスペクト比
        /// </summary>
        /// <remarks>
        /// 視野の縦横比(横/縦)です。
        /// </remarks>
        public float AspectRatio {
            get { return aspectRatio; }
            private set { this.aspectRatio = value; }
        }

        /// <summary>
        /// ニアクリップ面までの距離
        /// </summary>
        /// <remarks>
        /// カメラからニアクリップ面までのビュー空間での距離を返します。
        /// </remarks>
        public float NearClip {
            get { return nearClip; }
            private set { this.nearClip = value; }
        }

        /// <summary>
        /// ファークリップ面までの距離
        /// </summary>
        /// <remarks>
        /// カメラからファークリップ面までのビュー空間での距離を返します。
        /// </remarks>
        public float FarClip {
            get { return farClip; }
            private set { this.farClip = value; }
        }

        /// <summary>
        /// このカメラの投影行列
        /// </summary>
        /// <remarks>
        /// 現在のカメラの4x4の投影行列を取得します。
        /// </remarks>
        public Matrix4x4 ProjectionMatrix {
            get { return projMatrix; }
            private set { this.projMatrix = value; }
        }

        /// <summary>
        /// このオブジェクトが参照する全DDDオブジェクトを列挙する列挙子
        /// </summary>
        /// <remarks>
        /// 参照にnullは含まれません。同一のオブジェクトが複数出現する可能性があります。
        /// </remarks>
        public override IEnumerable<Object> References {
            get {
                return base.References;
            }
        }
        #endregion

        #region Method
        /// <summary>
        /// カメラを投影行列に変更し引数のパラメーターをセットします
        /// </summary>
        /// <param name="fovy">Y方向視野角(in degree)</param>
        /// <param name="aspectRatio">アスペクト比</param>
        /// <param name="near">ニアクリップ面</param>
        /// <param name="far">ファークリップ面</param>
        public void SetPerspective (float fovy, float aspectRatio, float near, float far) {
            if (fovy <= 0 || fovy >= 180) {
                throw new ArgumentException ("fovy is invalid");
            }
            if (aspectRatio <= 0) {
                throw new ArgumentException ("AspectRatio is invalid");
            }
            if (near <= 0) {
                throw new ArgumentException ("Near is invalid");
            }
            if (far <= 0) {
                throw new ArgumentException ("Far is invalid");
            }
            if (near >= far) {
                throw new ArgumentException ("Near and far are invalid");
            }
            this.type = ProjectionType.Perspective;
            this.fovy = fovy;
            this.aspectRatio = aspectRatio;
            this.nearClip = near;
            this.farClip = far;

            var h = (float)Math.Tan ((fovy/180.0f*(float)Math.PI) / 2.0f);
            var w = aspectRatio * h;
            this.projMatrix = new Matrix4x4 (1 / w, 0, 0, 0,
                                             0, 1 / h, 0, 0,
                                             0, 0, -(near + far) / (far - near), -2 * near * far / (far - near),
                                             0, 0, -1, 0);
        }

        /// <summary>
        /// カメラを平行投影に変更し引数のパラメーターをセットします
        /// </summary>
        /// <param name="fovy">Y方向視野(長さ)</param>
        /// <param name="aspectRatio">アスペクト比(横/縦)</param>
        /// <param name="near">ニアクリップ面</param>
        /// <param name="far">ファークリップ面</param>
        public void SetParallel (float fovy, float aspectRatio, float near, float far) {
            if (fovy <= 0) {
                throw new ArgumentException ("H is invalid");
            }
            if (aspectRatio <= 0) {
                throw new ArgumentException ("AspectRatio is invalid");
            }
            if (near <= 0) {
                throw new ArgumentException ("Near is invalid");
            }
            if (far <= 0) {
                throw new ArgumentException ("Far is invalid");
            }
            if (near >= far) {
                throw new ArgumentException ("Near and far are invalid");
            }
            this.type = ProjectionType.Parallel;
            this.fovy = fovy;
            this.aspectRatio = aspectRatio;
            this.nearClip = near;
            this.farClip = far;
            var h = fovy;
            var w = aspectRatio * fovy;
            this.projMatrix = new Matrix4x4 (2 / w, 0, 0, 0,
                                             0, 2 / h, 0, 0,
                                             0, 0, -2 / (far - near), -(near + far) / (far - near),
                                             0, 0, 0, 1);
        }

        /// <summary>
        /// カメラをスクリーン投影に変更し引数のパラメーターをセットします
        /// </summary>
        /// <param name="x">スクリーンのX座標</param>
        /// <param name="y">スクリーンのY座標</param>
        /// <param name="width">スクリーンの横方向の長さ(ピクセル数)</param>
        /// <param name="height">スクリーンの縦方向の長さ(ピクセル数)</param>
        public void SetScreen (float x, float y, float width, float height) {
            throw new NotImplementedException ("Sorry");
        }

        /// <summary>
        /// カメラを汎用に変更し任意の投影行列をセットします
        /// </summary>
        /// <remarks>
        /// 投影行列として任意の4x4の行列を使用します。
        /// 行列が投影行列として妥当な物である事を保証するのはユーザーの責任です。
        /// 少なくとも逆行列が計算できる必要があります。
        /// </remarks>
        /// <param name="matrix">投影行列</param>
        public void SetGeneric (Matrix4x4 matrix) {
            this.type = ProjectionType.Generic;
            this.fovy = 0;
            this.aspectRatio = 0;
            this.nearClip = 0;
            this.farClip = 0;
            this.ProjectionMatrix = matrix;
        }

        /// <summary>
        /// 現在のオブジェクトの状態を表す文字列を返します。
        /// </summary>

        public override string ToString () {
            return string.Format ("Camera: Type={0}, Fovy={1}, AspectRatio={2}, Near={3}, Far={4}", Type, Fovy, AspectRatio, NearClip, FarClip);
        }
        #endregion



    }
}
